home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Personal Computer World 2009 February
/
PCWFEB09.iso
/
Software
/
Linux
/
Kubuntu 8.10
/
kubuntu-8.10-desktop-i386.iso
/
casper
/
filesystem.squashfs
/
usr
/
share
/
hplip
/
base
/
vcard.py
< prev
Wrap
Text File
|
2008-10-13
|
45KB
|
1,432 lines
# -*- coding: utf-8 -*-
#
# (c) Copyright 2001-2008 Hewlett-Packard Development Company, L.P.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# ****************************************************************************
#
# Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the BitPim license as detailed in the LICENSE file.
#
# Code for reading and writing Vcard
#
# VCARD is defined in RFC 2425 and 2426
#
# Original author: Roger Binns <rogerb@rogerbinns.com>
# Modified for HPLIP by: Don Welch
#
# Local
from base.g import *
# Std Lib
import quopri
import base64
import codecs
import cStringIO
import re
import StringIO
import codecs
_boms = []
# 64 bit
try:
import encodings.utf_64
_boms.append( (codecs.BOM64_BE, "utf_64") )
_boms.append( (codecs.BOM64_LE, "utf_64") )
except: pass
# 32 bit
try:
import encodings.utf_32
_boms.append( (codecs.BOM_UTF32, "utf_32") )
_boms.append( (codecs.BOM_UTF32_BE, "utf_32") )
_boms.append( (codecs.BOM_UTF32_LE, "utf_32") )
except: pass
# 16 bit
try:
import encodings.utf_16
_boms.append( (codecs.BOM_UTF16, "utf_16") )
_boms.append( (codecs.BOM_UTF16_BE, "utf_16_be") )
_boms.append( (codecs.BOM_UTF16_LE, "utf_16_le") )
except: pass
# 8 bit
try:
import encodings.utf_8
_boms.append( (codecs.BOM_UTF8, "utf_8") )
except: pass
# Work arounds for Apple
_boms.append( ("\0B\0E\0G\0I\0N\0:\0V\0C\0A\0R\0D", "utf_16_be") )
_boms.append( ("B\0E\0G\0I\0N\0:\0V\0C\0A\0R\0D\0", "utf_16_le") )
# NB: the 32 bit and 64 bit versions have the BOM constants defined in Py 2.3
# but no corresponding encodings module. They are here for completeness.
# The order of above also matters since the first ones have longer
# boms than the latter ones, and we need to be unambiguous
_maxbomlen = max([len(bom) for bom,codec in _boms])
def opentextfile(name):
"""This function detects unicode byte order markers and if present
uses the codecs module instead to open the file instead with
appropriate unicode decoding, else returns the file using standard
open function"""
#with file(name, 'rb') as f:
f = file(name, 'rb')
start = f.read(_maxbomlen)
for bom,codec in _boms:
if start.startswith(bom):
# some codecs don't do readline, so we have to vector via stringio
# many postings also claim that the BOM is returned as the first
# character but that hasn't been the case in my testing
return StringIO.StringIO(codecs.open(name, "r", codec).read())
return file(name, "rtU")
_notdigits = re.compile("[^0-9]*")
_tendigits = re.compile("^[0-9]{10}$")
_sevendigits = re.compile("^[0-9]{7}$")
def phonenumber_normalise(n):
# this was meant to remove the long distance '1' prefix,
# temporary disable it, will be done on a phone-by-phone case.
return n
nums = "".join(re.split(_notdigits, n))
if len(nums) == 10:
return nums
if len(nums) == 11 and nums[0] == "1":
return nums[1:]
return n
def phonenumber_format(n):
if re.match(_tendigits, n) is not None:
return "(%s) %s-%s" % (n[0:3], n[3:6], n[6:])
elif re.match(_sevendigits, n) is not None:
return "%s-%s" %(n[:3], n[3:])
return n
def nameparser_formatsimplename(name):
"like L{formatname}, except we use the first matching component"
_fullname = nameparser_getfullname(name)
if _fullname:
return _fullname
return name.get('nickname', "")
def nameparser_getfullname(name):
"""Gets the full name, joining the first/middle/last if necessary"""
if name.has_key("full"):
return name["full"]
return ' '.join([x for x in nameparser_getparts(name) if x])
# See the following references for name parsing and how little fun it
# is.
#
# The simple way:
# http://cvs.gnome.org/lxr/source/evolution-data-server/addressbook/libebook/
# e-name-western*
#
# The "proper" way:
# http://cvs.xemacs.org/viewcvs.cgi/XEmacs/packages/xemacs-packages/mail-lib/mail-extr.el
#
# How we do it
#
# [1] The name is split into white-space seperated parts
# [2] If there is only one part, it becomes the firstname
# [3] If there are only two parts, they become first name and surname
# [4] For three or more parts, the first part is the first name and the last
# part is the surname. Then while the last part of the remainder starts with
# a lower case letter or is in the list below, it is prepended to the surname.
# Whatever is left becomes the middle name.
lastparts = [ "van", "von", "de", "di" ]
# I would also like to proudly point out that this code has no comment saying
# "Have I no shame". It will be considered incomplete until that happens
def nameparser_getparts_FML(name):
n = name.get("full")
# [1]
parts = n.split()
# [2]
if len(parts) <= 1:
return (n, "", "")
# [3]
if len(parts) == 2:
return (parts[0], "", parts[1])
# [4]
f = [parts[0]]
m = []
l = [parts[-1]]
del parts[0]
del parts[-1]
while len(parts) and (parts[-1][0].lower() == parts[-1][0] or parts[-1].lower() in lastparts):
l = [parts[-1]]+l
del parts[-1]
m = parts
# return it all
return (" ".join(f), " ".join(m), " ".join(l))
def nameparser_getparts_LFM(name):
n = name.get("full")
parts = n.split(',')
if len(parts) <= 1:
return (n, '', '')
_last = parts[0]
_first = ''
_middle = ''
parts = parts[1].split()
if len(parts) >= 1:
_first = parts[0]
if len(parts) > 1:
_middle = ' '.join(parts[1:])
return (_first, _middle, _last)
def nameparser_getparts(name):
"""Returns (first, middle, last) for name. If the part doesn't exist
then a blank string is returned"""
# do we have any of the parts?
for i in ("first", "middle", "last"):
if name.has_key(i):
return (name.get("first", ""), name.get("middle", ""), name.get("last", ""))
# check we have full. if not return nickname
if not name.has_key("full"):
return (name.get("nickname", ""), "", "")
n = name.nameparser_get("full")
if ',' in n:
return nameparser_getparts_LFM(name)
return nameparser_getparts_FML(name)
class VFileException(Exception):
pass
class VFile:
_charset_aliases = {
'MACINTOSH': 'MAC_ROMAN'
}
def __init__(self, source):
self.source = source
self.saved = None
def __iter__(self):
return self
def next(self):
# Get the next non-blank line
while True: # python desperately needs do-while
line = self._getnextline()
if line is None:
raise StopIteration()
if len(line) != 0:
break
# Hack for evolution. If ENCODING is QUOTED-PRINTABLE then it doesn't
# offset the next line, so we look to see what the first char is
normalcontinuations = True
colon = line.find(':')
if colon > 0:
s = line[:colon].lower().split(";")
if "quoted-printable" in s or 'encoding=quoted-printable' in s:
normalcontinuations = False
while line[-1] == "=" or line[-2] == '=':
if line[-1] == '=':
i = -1
else:
i = -2
nextl = self._getnextline()
if nextl[0] in ("\t", " "): nextl = nextl[1:]
line = line[:i]+nextl
while normalcontinuations:
nextline = self._lookahead()
if nextline is None:
break
if len(nextline) == 0:
break
if nextline[0] != ' ' and nextline[0] != '\t':
break
line += self._getnextline()[1:]
colon = line.find(':')
if colon < 1:
# some evolution vcards don't even have colons
# raise VFileException("Invalid property: "+line)
log.debug("Fixing up bad line: %s" % line)
colon = len(line)
line += ":"
b4 = line[:colon]
line = line[colon+1:].strip()
# upper case and split on semicolons
items = b4.upper().split(";")
newitems = []
if isinstance(line, unicode):
charset = None
else:
charset = "LATIN-1"
for i in items:
# ::TODO:: probably delete anything preceding a '.'
# (see 5.8.2 in rfc 2425)
# look for charset parameter
if i.startswith("CHARSET="):
charset = i[8:] or "LATIN-1"
continue
# unencode anything that needs it
if not i.startswith("ENCODING=") and not i=="QUOTED-PRINTABLE": # evolution doesn't bother with "ENCODING="
# ::TODO:: deal with backslashes, being especially careful with ones quoting semicolons
newitems.append(i)
continue
try:
if i == 'QUOTED-PRINTABLE' or i == "ENCODING=QUOTED-PRINTABLE":
# technically quoted printable is ascii only but we decode anyway since not all vcards comply
line = quopri.decodestring(line)
elif i == 'ENCODING=B':
line = base64.decodestring(line)
charset = None
else:
raise VFileException("unknown encoding: "+i)
except Exception,e:
if isinstance(e,VFileException):
raise e
raise VFileException("Exception %s while processing encoding %s on data '%s'" % (str(e), i, line))
# ::TODO:: repeat above shenanigans looking for a VALUE= thingy and
# convert line as in 5.8.4 of rfc 2425
if len(newitems) == 0:
raise VFileException("Line contains no property: %s" % (line,))
# charset frigging
if charset is not None:
try:
decoder = codecs.getdecoder(self._charset_aliases.get(charset, charset))
line,_ = decoder(line)
except LookupError:
raise VFileException("unknown character set '%s' in parameters %s" % (charset, b4))
if newitems == ["BEGIN"] or newitems == ["END"]:
line = line.upper()
return newitems, line
def _getnextline(self):
if self.saved is not None:
line = self.saved
self.saved = None
return line
else:
return self._readandstripline()
def _readandstripline(self):
line = self.source.readline()
if line is not None:
if len(line) == 0:
return None
elif line[-2:] == "\r\n":
return line[:-2]
elif line[-1] == '\r' or line[-1] == '\n':
return line[:-1]
return line
def _lookahead(self):
assert self.saved is None
self.saved = self._readandstripline()
return self.saved
class VCards:
"Understands vcards in a vfile"
def __init__(self, vfile):
self.vfile = vfile
def __iter__(self):
return self
def next(self):
# find vcard start
field = value = None
for field,value in self.vfile:
if (field,value) != (["BEGIN"], "VCARD"):
continue
found = True
break
if (field,value) != (["BEGIN"], "VCARD"):
# hit eof without any BEGIN:vcard
raise StopIteration()
# suck up lines
lines = []
for field,value in self.vfile:
if (field,value) != (["END"], "VCARD"):
lines.append( (field,value) )
continue
break
if (field,value) != (["END"], "VCARD"):
raise VFileException("There is a BEGIN:VCARD but no END:VCARD")
return VCard(lines)
class VCard:
"A single vcard"
def __init__(self, lines):
self._version = (2,0) # which version of the vcard spec the card conforms to
self._origin = None # which program exported the vcard
self._data = {}
self._groups = {}
self.lines = []
# extract version field
for f,v in lines:
assert len(f)
if f == ["X-EVOLUTION-FILE-AS"]: # all evolution cards have this
self._origin = "evolution"
if f[0].startswith("ITEM") and (f[0].endswith(".X-ABADR") or f[0].endswith(".X-ABLABEL")):
self._origin = "apple"
if len(v) and v[0].find(">!$_") > v[0].find("_$!<") >= 0:
self.origin = "apple"
if f == ["VERSION"]:
ver = v.split(".")
try:
ver = [int(xx) for xx in ver]
except ValueError:
raise VFileException(v+" is not a valid vcard version")
self._version = ver
continue
# convert {home,work}.{tel,label} to {tel,label};{home,work}
# this probably dates from *very* early vcards
if f[0] == "HOME.TEL":
f[0:1] = ["TEL", "HOME"]
elif f[0] == "HOME.LABEL":
f[0:1] = ["LABEL", "HOME"]
elif f[0] == "WORK.TEL":
f[0:1] = ["TEL", "WORK"]
elif f[0] == "WORK.LABEL":
f[0:1] = ["LABEL", "WORK"]
self.lines.append( (f,v) )
self._parse(self.lines, self._data)
self._update_groups(self._data)
def getdata(self):
"Returns a dict of the data parsed out of the vcard"
return self._data
def get(self, key, default=''):
return self._data.get(key, default)
def _getfieldname(self, name, dict):
"""Returns the fieldname to use in the dict.
For example, if name is "email" and there is no "email" field
in dict, then "email" is returned. If there is already an "email"
field then "email2" is returned, etc"""
if name not in dict:
return name
for i in xrange(2,99999):
if name+`i` not in dict:
return name+`i`
def _parse(self, lines, result):
for field,value in lines:
if len(value.strip()) == 0: # ignore blank values
continue
if '.' in field[0]:
f = field[0][field[0].find('.')+1:]
else:
f = field[0]
t = f.replace("-", "_")
func = getattr(self, "_field_"+t, self._default_field)
func(field, value, result)
def _update_groups(self, result):
"""Update the groups info """
for k,e in self._groups.items():
self._setvalue(result, *e)
# fields we ignore
def _field_ignore(self, field, value, result):
pass
_field_LABEL = _field_ignore # we use the ADR field instead
_field_BDAY = _field_ignore # not stored in bitpim
_field_ROLE = _field_ignore # not stored in bitpim
_field_CALURI = _field_ignore # not stored in bitpim
_field_CALADRURI = _field_ignore # variant of above
_field_FBURL = _field_ignore # not stored in bitpim
_field_REV = _field_ignore # not stored in bitpim
_field_KEY = _field_ignore # not stored in bitpim
_field_SOURCE = _field_ignore # not stored in bitpim (although arguably part of serials)
_field_PHOTO = _field_ignore # contained either binary image, or external URL, not used by BitPim
# simple fields
def _field_FN(self, field, value, result):
result[self._getfieldname("name", result)] = self.unquote(value)
def _field_TITLE(self, field, value, result):
result[self._getfieldname("title", result)] = self.unquote(value)
def _field_NICKNAME(self, field, value, result):
# ::TODO:: technically this is a comma seperated list ..
result[self._getfieldname("nickname", result)] = self.unquote(value)
def _field_NOTE(self, field, value, result):
result[self._getfieldname("notes", result)] = self.unquote(value)
def _field_UID(self, field, value, result):
result["uid"] = self.unquote(value) # note that we only store one UID (the "U" does stand for unique)
#
# Complex fields
#
def _field_N(self, field, value, result):
value = self.splitandunquote(value)
familyname = givenname = additionalnames = honorificprefixes = honorificsuffixes = None
try:
familyname = value[0]
givenname = value[1]
additionalnames = value[2]
honorificprefixes = value[3]
honorificsuffixes = value[4]
except IndexError:
pass
if familyname is not None and len(familyname):
result[self._getfieldname("last name", result)] = familyname
if givenname is not None and len(givenname):
result[self._getfieldname("first name", result)] = givenname
if additionalnames is not None and len(additionalnames):
result[self._getfieldname("middle name", result)] = additionalnames
if honorificprefixes is not None and len(honorificprefixes):
result[self._getfieldname("prefix", result)] = honorificprefixes
if honorificsuffixes is not None and len(honorificsuffixes):
result[self._getfieldname("suffix", result)] = honorificsuffixes
_field_NAME = _field_N # early versions of vcard did this
def _field_ORG(self, field, value, result):
value = self.splitandunquote(value)
if len(value):
result[self._getfieldname("organisation", result)] = value[0]
for f in value[1:]:
result[self._getfieldname("organisational unit", result)] = f
_field_O = _field_ORG # early versions of vcard did this
def _field_EMAIL(self, field, value, result):
value = self.unquote(value)
# work out the types
types = []
for f in field[1:]:
if f.startswith("TYPE="):
ff = f[len("TYPE="):].split(",")
else:
ff = [f]
types.extend(ff)
# the standard doesn't specify types of "home" and "work" but
# does allow for random user defined types, so we look for them
type = None
for t in types:
if t == "HOME":
type="home"
if t == "WORK":
type="business"
if t == "X400":
return # we don't want no steenking X.400
preferred = "PREF" in types
if type is None:
self._setvalue(result, "email", value, preferred)
else:
addr = {'email': value, 'type': type}
self._setvalue(result, "email", addr, preferred)
def _field_URL(self, field, value, result):
# the standard doesn't specify url types or a pref type,
# but we implement it anyway
value = self.unquote(value)
# work out the types
types = []
for f in field[1:]:
if f.startswith("TYPE="):
ff = f[len("TYPE="):].split(",")
else:
ff=[f]
types.extend(ff)
type = None
for t in types:
if t == "HOME":
type="home"
if t == "WORK":
type="business"
preferred = "PREF" in types
if type is None:
self._setvalue(result, "url", value, preferred)
else:
addr = {'url': value, 'type': type}
self._setvalue(result, "url", addr, preferred)
def _field_X_SPEEDDIAL(self, field, value, result):
if '.' in field[0]:
group = field[0][:field[0].find('.')]
else:
group = None
if group is None:
# this has to belong to a group!!
#print 'speedial has no group'
log.debug("speeddial has no group")
else:
self._setgroupvalue(result, 'phone', { 'speeddial': int(value) },
group, False)
def _field_TEL(self, field, value, result):
value = self.unquote(value)
# see if this is part of a group
if '.' in field[0]:
group = field[0][:field[0].find('.')]
else:
group = None
# work out the types
types = []
for f in field[1:]:
if f.startswith("TYPE="):
ff = f[len("TYPE="):].split(",")
else:
ff = [f]
types.extend(ff)
# type munging - we map vcard types to simpler ones
munge = { "BBS": "DATA", "MODEM": "DATA", "ISDN": "DATA", "CAR": "CELL",
"PCS": "CELL" }
types = [munge.get(t, t) for t in types]
# reduce types to home, work, msg, pref, voice, fax, cell, video, pager, data
types = [t for t in types if t in ("HOME", "WORK", "MSG", "PREF", "VOICE",
"FAX", "CELL", "VIDEO", "PAGER", "DATA")]
# if type is in this list and voice not explicitly mentioned then it is not a voice type
antivoice = ["FAX", "PAGER", "DATA"]
if "VOICE" in types:
voice = True
else:
voice = True # default is voice
for f in antivoice:
if f in types:
voice = False
break
preferred = "PREF" in types
# vcard allows numbers to be multiple things at the same time, such as home voice, home fax
# and work fax so we have to test for all variations
# if neither work or home is specified, then no default (otherwise things get really complicated)
iswork = False
ishome = False
if "WORK" in types:
iswork = True
if "HOME" in types:
ishome = True
if len(types) == 0 or types == ["PREF"]:
iswork = True # special case when nothing else is specified
value = phonenumber_normalise(value)
if iswork and voice:
self._setgroupvalue(result,
"phone", {"type": "business", "number": value},
group, preferred)
if ishome and voice:
self._setgroupvalue(result,
"phone", {"type": "home", "number": value},
group, preferred)
if not iswork and not ishome and "FAX" in types:
# fax without explicit work or home
self._setgroupvalue(result,
"phone", {"type": "fax", "number": value},
group, preferred)
else:
if iswork and "FAX" in types:
self._setgroupvalue(result, "phone",
{"type": "business fax", "number": value},
group, preferred)
if ishome and "FAX" in types:
self._setgroupvalue(result, "phone",
{"type": "home fax", "number": value},
group, preferred)
if "CELL" in types:
self._setgroupvalue(result,
"phone", {"type": "cell", "number": value},
group, preferred)
if "PAGER" in types:
self._setgroupvalue(result,
"phone", {"type": "pager", "number": value},
group, preferred)
if "DATA" in types:
self._setgroupvalue(result,
"phone", {"type": "data", "number": value},
group, preferred)
def _setgroupvalue(self, result, type, value, group, preferred=False):
""" Set value of an item of a group
"""
if group is None:
# no groups specified
return self._setvalue(result, type, value, preferred)
group_type = self._groups.get(group, None)
if group_type is None:
# 1st one of the group
self._groups[group] = [type, value, preferred]
else:
if type != group_type[0]:
log.debug('Group %s has different types: %s, %s' % (group, type,groups_type[0]))
if preferred:
group_type[2] = True
group_type[1].update(value)
def _setvalue(self, result, type, value, preferred=False):
if type not in result:
result[type] = value
return
if not preferred:
result[self._getfieldname(type, result)] = value
return
# we need to insert our value at the begining
values = [value]
for suffix in [""]+range(2,99):
if type+str(suffix) in result:
values.append(result[type+str(suffix)])
else:
break
suffixes = [""]+range(2,len(values)+1)
for l in range(len(suffixes)):
result[type+str(suffixes[l])] = values[l]
def _field_CATEGORIES(self, field, value, result):
# comma seperated just for fun
values = self.splitandunquote(value, seperator=",")
values = [v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field
values = [v for v in values if len(v)]
v = result.get('categories', None)
if v:
result['categories'] = ';'.join([v, ";".join(values)])
else:
result['categories'] = ';'.join(values)
def _field_SOUND(self, field, value, result):
# comma seperated just for fun
values = self.splitandunquote(value, seperator=",")
values = [v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field
values = [v for v in values if len(v)]
result[self._getfieldname("ringtones", result)] = ";".join(values)
_field_CATEGORY = _field_CATEGORIES # apple use "category" which is not in the spec
def _field_ADR(self, field, value, result):
# work out the type
preferred = False
type = "business"
for f in field[1:]:
if f.startswith("TYPE="):
ff = f[len("TYPE="):].split(",")
else:
ff = [f]
for x in ff:
if x == "HOME":
type = "home"
if x == "PREF":
preferred = True
value = self.splitandunquote(value)
pobox = extendedaddress = streetaddress = locality = region = postalcode = country = None
try:
pobox = value[0]
extendedaddress = value[1]
streetaddress = value[2]
locality = value[3]
region = value[4]
postalcode = value[5]
country = value[6]
except IndexError:
pass
addr = {}
if pobox is not None and len(pobox):
addr["pobox"] = pobox
if extendedaddress is not None and len(extendedaddress):
addr["street2"] = extendedaddress
if streetaddress is not None and len(streetaddress):
addr["street"] = streetaddress
if locality is not None and len(locality):
addr["city"] = locality
if region is not None and len(region):
addr["state"] = region
if postalcode is not None and len(postalcode):
addr["postalcode"] = postalcode
if country is not None and len(country):
addr["country"] = country
if len(addr):
addr["type"] = type
self._setvalue(result, "address", addr, preferred)
def _field_X_PALM(self, field, value, result):
# handle a few PALM custom fields
ff = field[0].split(".")
f0 = ff[0]
if len(ff) > 1:
f1 = ff[1]
else:
f1 = ''
if f0.startswith('X-PALM-CATEGORY') or f1.startswith('X-PALM-CATEGORY'):
self._field_CATEGORIES(['CATEGORIES'], value, result)
elif f0 == 'X-PALM-NICKNAME' or f1 == 'X-PALM-NICKNAME':
self._field_NICKNAME(['NICKNAME'], value, result)
else:
log.debug("Ignoring PALM custom field: %s" % field)
def _default_field(self, field, value, result):
ff = field[0].split(".")
f0 = ff[0]
if len(ff) > 1:
f1 = ff[1]
else:
f1 = ''
if f0.startswith('X-PALM-') or f1.startswith('X-PALM-'):
self._field_X_PALM(field, value, result)
return
elif f0.startswith("X-") or f1.startswith("X-"):
log.debug("Ignoring custom field: %s" % field)
return
log.debug("No idea what to do with %s (%s)" % (field, value[:80]))
def unquote(self, value):
# ::TODO:: do this properly (deal with all backslashes)
return value.replace(r"\;", ";") \
.replace(r"\,", ",") \
.replace(r"\n", "\n") \
.replace(r"\r\n", "\r\n") \
.replace("\r\n", "\n") \
.replace("\r", "\n")
def splitandunquote(self, value, seperator=";"):
# also need a splitandsplitandunquote since some ; delimited fields are then comma delimited
# short cut for normal case - no quoted seperators
if value.find("\\"+seperator)<0:
return [self.unquote(v) for v in value.split(seperator)]
# funky quoting, do it the slow hard way
res = []
build = ""
v = 0
while v < len(value):
if value[v] == seperator:
res.append(build)
build = ""
v += 1
continue
if value[v] == "\\":
build += value[v:v+2]
v += 2
continue
build += value[v]
v += 1
if len(build):
res.append(build)
return [self.unquote(v) for v in res]
def version(self):
"Best guess as to vcard version"
return self._version
def origin(self):
"Best guess as to what program wrote the vcard"
return self._origin
def __getitem__(self, item):
return self._data[item]
def __repr__(self):
return repr(self._data)
# The formatters return a string
def myqpencodestring(value):
"""My own routine to do qouted printable since the builtin one doesn't encode CR or NL!"""
return quopri.encodestring(value).replace("\r", "=0D").replace("\n", "=0A")
def format_stringv2(value):
"""Return a vCard v2 string. Any embedded commas or semi-colons are removed."""
return value.replace("\\", "").replace(",", "").replace(";", "")
def format_stringv3(value):
"""Return a vCard v3 string. Embedded commas and semi-colons are backslash quoted"""
return value.replace("\\", "").replace(",", r"\,").replace(";", r"\;")
_string_formatters = (format_stringv2, format_stringv3)
def format_binary(value):
"""Return base 64 encoded string"""
# encodestring always adds a newline so we have to strip it off
return base64.encodestring(value).rstrip()
def _is_sequence(v):
"""Determine if v is a sequence such as passed to value in out_line.
Note that a sequence of chars is not a sequence for our purposes."""
return isinstance(v, (type( () ), type([])))
def out_line(name, attributes, value, formatter, join_char=";"):
"""Returns a single field correctly formatted and encoded (including trailing newline)
@param name: The field name
@param attributes: A list of string attributes (eg "TYPE=intl,post" ). Usually
empty except for TEL and ADR. You can also pass in None.
@param value: The field value. You can also pass in a list of components which will be
joined with join_char such as the 6 components of N
@param formatter: The function that formats the value/components. See the
various format_ functions. They will automatically ensure that
ENCODING=foo attributes are added if appropriate"""
if attributes is None:
attributes = [] # ensure it is a list
else:
attributes = list(attributes[:]) # ensure we work with a copy
if formatter in _string_formatters:
if _is_sequence(value):
qp = False
for f in value:
f = formatter(f)
if myqpencodestring(f) != f:
qp = True
break
if qp:
attributes.append("ENCODING=QUOTED-PRINTABLE")
value = [myqpencodestring(f) for f in value]
value = join_char.join(value)
else:
value = formatter(value)
# do the qp test
qp = myqpencodestring(value) != value
if qp:
value = myqpencodestring(value)
attributes.append("ENCODING=QUOTED-PRINTABLE")
else:
assert not _is_sequence(value)
if formatter is not None:
value = formatter(value) # ::TODO:: deal with binary and other formatters and their encoding types
res = ";".join([name]+attributes)+":"
res += _line_reformat(value, 70, 70-len(res))
assert res[-1] != "\n"
return res+"\n"
def _line_reformat(line, width=70, firstlinewidth=0):
"""Takes line string and inserts newlines
and spaces on following continuation lines
so it all fits in width characters
@param width: how many characters to fit it in
@param firstlinewidth: if >0 then first line is this width.
if equal to zero then first line is same width as rest.
if <0 then first line will go immediately to continuation.
"""
if firstlinewidth == 0:
firstlinewidth = width
if len(line) < firstlinewidth:
return line
res = ""
if firstlinewidth > 0:
res += line[:firstlinewidth]
line = line[firstlinewidth:]
while len(line):
res += "\n "+line[:width]
if len(line)<width:
break
line = line[width:]
return res
def out_names(vals, formatter, limit=1):
res = ""
for v in vals[:limit]:
# full name
res += out_line("FN", None, nameparser_formatsimplename(v), formatter)
# name parts
f,m,l = nameparser_getparts(v)
res += out_line("N", None, (l,f,m,"",""), formatter)
# nickname
nn = v.get("nickname", "")
if len(nn):
res += out_line("NICKNAME", None, nn, formatter)
return res
# Apple uses wrong field name so we do some futzing ...
def out_categories(vals, formatter, field="CATEGORIES"):
cats = [v.get("category") for v in vals]
if len(cats):
return out_line(field, None, cats, formatter, join_char=",")
return ""
def out_categories_apple(vals, formatter):
return out_categories(vals, formatter, field="CATEGORY")
# Used for both email and urls. we don't put any limits on how many are output
def out_eu(vals, formatter, field, bpkey):
res = ""
first = True
for v in vals:
val = v.get(bpkey)
type = v.get("type", "")
if len(type):
if type == "business":
type = "work" # vcard uses different name
type = type.upper()
if first:
type = type+",PREF"
elif first:
type = "PREF"
if len(type):
type = ["TYPE="+type+["",",INTERNET"][field == "EMAIL"]] # email also has "INTERNET"
else:
type = None
res += out_line(field, type, val, formatter)
first = False
return res
def out_emails(vals, formatter):
return out_eu(vals, formatter, "EMAIL", "email")
def out_urls(vals, formatter):
return out_eu(vals, formatter, "URL", "url")
_out_tel_mapping = {
'home': 'HOME',
'office': 'WORK',
'cell': 'CELL',
'fax': 'FAX',
'pager': 'PAGER',
'data': 'MODEM',
'none': 'VOICE'
}
def out_tel(vals, formatter):
# ::TODO:: limit to one type of each number
phones = ['phone'+str(x) for x in ['']+range(2,len(vals)+1)]
res = ""
first = True
idx = 0
for v in vals:
sp = v.get('speeddial', None)
if sp is None:
# no speed dial
res += out_line("TEL",
["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
phonenumber_format(v['number']), formatter)
else:
res += out_line(phones[idx]+".TEL",
["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
phonenumber_format(v['number']), formatter)
res += out_line(phones[idx]+".X-SPEEDDIAL", None, str(sp), formatter)
idx += 1
first = False
return res
# and addresses
def out_adr(vals, formatter):
# ::TODO:: limit to one type of each address, and only one org
res = ""
first = True
for v in vals:
o = v.get("company", "")
if len(o):
res += out_line("ORG", None, o, formatter)
if v.get("type") == "home":
type = "HOME"
else:
type = "WORK"
type = "TYPE="+type+("", ",PREF")[first]
res += out_line("ADR", [type], [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], formatter)
first = False
return res
def out_note(vals, formatter, limit=1):
return "".join([out_line("NOTE", None, v["memo"], formatter) for v in vals[:limit]])
# Sany SCP-6600 (Katana) support
def out_tel_scp6600(vals, formatter):
res = ""
_pref = len(vals) > 1
if _pref:
s = "PREF,"
else:
s = ''
for v in vals:
res += out_line("TEL", s,
["TYPE=%s%s" % (s, _out_tel_mapping[v['type']])],
phonenumber_format(v['number']), formatter)
_pref = False
return res
def out_email_scp6600(vals, formatter):
res = ''
for _idx in range(min(len(vals), 2)):
v = vals[_idx]
if v.get('email', None):
res += out_line('EMAIL', ['TYPE=INTERNET'],
v['email'], formatter)
return res
def out_url_scp660(vals, formatter):
if vals and vals[0].get('url', None):
return out_line('URL', None, vals[0]['url'], formatter)
return ''
def out_adr_scp6600(vals, formatter):
for v in vals:
if v.get('type', None) == 'home':
_type = 'HOME'
else:
_type = 'WORK'
return out_line("ADR", ['TYPE=%s' % _type],
[v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")],
formatter)
return ''
# This is the order we write things out to the vcard. Although
# vCard doesn't require an ordering, it looks nicer if it
# is (eg name first)
_field_order = ("names", "wallpapers", "addresses", "numbers", "categories",
"emails", "urls", "ringtones", "flags", "memos", "serials")
def output_entry(entry, profile, limit_fields=None):
# debug build assertion that limit_fields only contains fields we know about
if __debug__ and limit_fields is not None:
assert len([f for f in limit_fields if f not in _field_order]) == 0
fmt = profile["_formatter"]
io = cStringIO.StringIO()
io.write(out_line("BEGIN", None, "VCARD", None))
io.write(out_line("VERSION", None, profile["_version"], None))
if limit_fields is None:
fields = _field_order
else:
fields = [f for f in _field_order if f in limit_fields]
for f in fields:
if f in entry and f in profile:
func = profile[f]
# does it have a limit? (nice scary introspection :-)
if "limit" in func.func_code.co_varnames[:func.func_code.co_argcount]:
lines = func(entry[f], fmt, limit = profile["_limit"])
else:
lines = func(entry[f], fmt)
if len(lines):
io.write(lines)
io.write(out_line("END", None, "VCARD", fmt))
return io.getvalue()
profile_vcard2 = {
'_formatter': format_stringv2,
'_limit': 1,
'_version': "2.1",
'names': out_names,
'categories': out_categories,
'emails': out_emails,
'urls': out_urls,
'numbers': out_tel,
'addresses': out_adr,
'memos': out_note,
}
profile_vcard3 = profile_vcard2.copy()
profile_vcard3['_formatter'] = format_stringv3
profile_vcard3['_version'] = "3.0"
profile_apple = profile_vcard3.copy()
profile_apple['categories'] = out_categories_apple
profile_full = profile_vcard3.copy()
profile_full['_limit'] = 99999
profile_scp6600 = profile_full.copy()
del profile_scp6600['categories']
profile_scp6600.update(
{ 'numbers': out_tel_scp6600,
'emails': out_email_scp6600,
'urls': out_url_scp660,
'addresses': out_adr_scp6600,
})
profiles = {
'vcard2': { 'description': "vCard v2.1", 'profile': profile_vcard2 },
'vcard3': { 'description': "vCard v3.0", 'profile': profile_vcard3 },
'apple': { 'description': "Apple", 'profile': profile_apple },
'fullv3': { 'description': "Full vCard v3.0", 'profile': profile_full},
'scp6600': { 'description': "Sanyo SCP-6600 (Katana)",
'profile': profile_scp6600 },
}